/*
 * Copyright (c) 1998, 2008, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package javax.swing.text.html;

import java.awt.Polygon;
import java.io.Serializable;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.swing.text.AttributeSet;

/**
 * Map is used to represent a map element that is part of an HTML document.
 * Once a Map has been created, and any number of areas have been added,
 * you can test if a point falls inside the map via the contains method.
 *
 * @author Scott Violet
 */
class Map implements Serializable {

  /**
   * Name of the Map.
   */
  private String name;
  /**
   * An array of AttributeSets.
   */
  private Vector<AttributeSet> areaAttributes;
  /**
   * An array of RegionContainments, will slowly grow to match the
   * length of areaAttributes as needed.
   */
  private Vector<RegionContainment> areas;

  public Map() {
  }

  public Map(String name) {
    this.name = name;
  }

  /**
   * Returns the name of the Map.
   */
  public String getName() {
    return name;
  }

  /**
   * Defines a region of the Map, based on the passed in AttributeSet.
   */
  public void addArea(AttributeSet as) {
    if (as == null) {
      return;
    }
    if (areaAttributes == null) {
      areaAttributes = new Vector<AttributeSet>(2);
    }
    areaAttributes.addElement(as.copyAttributes());
  }

  /**
   * Removes the previously created area.
   */
  public void removeArea(AttributeSet as) {
    if (as != null && areaAttributes != null) {
      int numAreas = (areas != null) ? areas.size() : 0;
      for (int counter = areaAttributes.size() - 1; counter >= 0;
          counter--) {
        if (areaAttributes.elementAt(counter).isEqual(as)) {
          areaAttributes.removeElementAt(counter);
          if (counter < numAreas) {
            areas.removeElementAt(counter);
          }
        }
      }
    }
  }

  /**
   * Returns the AttributeSets representing the differet areas of the Map.
   */
  public AttributeSet[] getAreas() {
    int numAttributes = (areaAttributes != null) ? areaAttributes.size() :
        0;
    if (numAttributes != 0) {
      AttributeSet[] retValue = new AttributeSet[numAttributes];

      areaAttributes.copyInto(retValue);
      return retValue;
    }
    return null;
  }

  /**
   * Returns the AttributeSet that contains the passed in location,
   * <code>x</code>, <code>y</code>. <code>width</code>, <code>height</code>
   * gives the size of the region the map is defined over. If a matching
   * area is found, the AttribueSet for it is returned.
   */
  public AttributeSet getArea(int x, int y, int width, int height) {
    int numAttributes = (areaAttributes != null) ?
        areaAttributes.size() : 0;

    if (numAttributes > 0) {
      int numAreas = (areas != null) ? areas.size() : 0;

      if (areas == null) {
        areas = new Vector<RegionContainment>(numAttributes);
      }
      for (int counter = 0; counter < numAttributes; counter++) {
        if (counter >= numAreas) {
          areas.addElement(createRegionContainment
              (areaAttributes.elementAt(counter)));
        }
        RegionContainment rc = areas.elementAt(counter);
        if (rc != null && rc.contains(x, y, width, height)) {
          return areaAttributes.elementAt(counter);
        }
      }
    }
    return null;
  }

  /**
   * Creates and returns an instance of RegionContainment that can be
   * used to test if a particular point lies inside a region.
   */
  protected RegionContainment createRegionContainment
  (AttributeSet attributes) {
    Object shape = attributes.getAttribute(HTML.Attribute.SHAPE);

    if (shape == null) {
      shape = "rect";
    }
    if (shape instanceof String) {
      String shapeString = ((String) shape).toLowerCase();
      RegionContainment rc = null;

      try {
        if (shapeString.equals("rect")) {
          rc = new RectangleRegionContainment(attributes);
        } else if (shapeString.equals("circle")) {
          rc = new CircleRegionContainment(attributes);
        } else if (shapeString.equals("poly")) {
          rc = new PolygonRegionContainment(attributes);
        } else if (shapeString.equals("default")) {
          rc = DefaultRegionContainment.sharedInstance();
        }
      } catch (RuntimeException re) {
        // Something wrong with attributes.
        rc = null;
      }
      return rc;
    }
    return null;
  }

  /**
   * Creates and returns an array of integers from the String
   * <code>stringCoords</code>. If one of the values represents a
   * % the returned value with be negative. If a parse error results
   * from trying to parse one of the numbers null is returned.
   */
  static protected int[] extractCoords(Object stringCoords) {
    if (stringCoords == null || !(stringCoords instanceof String)) {
      return null;
    }

    StringTokenizer st = new StringTokenizer((String) stringCoords,
        ", \t\n\r");
    int[] retValue = null;
    int numCoords = 0;

    while (st.hasMoreElements()) {
      String token = st.nextToken();
      int scale;

      if (token.endsWith("%")) {
        scale = -1;
        token = token.substring(0, token.length() - 1);
      } else {
        scale = 1;
      }
      try {
        int intValue = Integer.parseInt(token);

        if (retValue == null) {
          retValue = new int[4];
        } else if (numCoords == retValue.length) {
          int[] temp = new int[retValue.length * 2];

          System.arraycopy(retValue, 0, temp, 0, retValue.length);
          retValue = temp;
        }
        retValue[numCoords++] = intValue * scale;
      } catch (NumberFormatException nfe) {
        return null;
      }
    }
    if (numCoords > 0 && numCoords != retValue.length) {
      int[] temp = new int[numCoords];

      System.arraycopy(retValue, 0, temp, 0, numCoords);
      retValue = temp;
    }
    return retValue;
  }


  /**
   * Defines the interface used for to check if a point is inside a
   * region.
   */
  interface RegionContainment {

    /**
     * Returns true if the location <code>x</code>, <code>y</code>
     * falls inside the region defined in the receiver.
     * <code>width</code>, <code>height</code> is the size of
     * the enclosing region.
     */
    public boolean contains(int x, int y, int width, int height);
  }


  /**
   * Used to test for containment in a rectangular region.
   */
  static class RectangleRegionContainment implements RegionContainment {

    /**
     * Will be non-null if one of the values is a percent, and any value
     * that is non null indicates it is a percent
     * (order is x, y, width, height).
     */
    float[] percents;
    /**
     * Last value of width passed in.
     */
    int lastWidth;
    /**
     * Last value of height passed in.
     */
    int lastHeight;
    /**
     * Top left.
     */
    int x0;
    int y0;
    /**
     * Bottom right.
     */
    int x1;
    int y1;

    public RectangleRegionContainment(AttributeSet as) {
      int[] coords = Map.extractCoords(as.getAttribute(HTML.
          Attribute.COORDS));

      percents = null;
      if (coords == null || coords.length != 4) {
        throw new RuntimeException("Unable to parse rectangular area");
      } else {
        x0 = coords[0];
        y0 = coords[1];
        x1 = coords[2];
        y1 = coords[3];
        if (x0 < 0 || y0 < 0 || x1 < 0 || y1 < 0) {
          percents = new float[4];
          lastWidth = lastHeight = -1;
          for (int counter = 0; counter < 4; counter++) {
            if (coords[counter] < 0) {
              percents[counter] = Math.abs
                  (coords[counter]) / 100.0f;
            } else {
              percents[counter] = -1.0f;
            }
          }
        }
      }
    }

    public boolean contains(int x, int y, int width, int height) {
      if (percents == null) {
        return contains(x, y);
      }
      if (lastWidth != width || lastHeight != height) {
        lastWidth = width;
        lastHeight = height;
        if (percents[0] != -1.0f) {
          x0 = (int) (percents[0] * width);
        }
        if (percents[1] != -1.0f) {
          y0 = (int) (percents[1] * height);
        }
        if (percents[2] != -1.0f) {
          x1 = (int) (percents[2] * width);
        }
        if (percents[3] != -1.0f) {
          y1 = (int) (percents[3] * height);
        }
      }
      return contains(x, y);
    }

    public boolean contains(int x, int y) {
      return ((x >= x0 && x <= x1) &&
          (y >= y0 && y <= y1));
    }
  }


  /**
   * Used to test for containment in a polygon region.
   */
  static class PolygonRegionContainment extends Polygon implements
      RegionContainment {

    /**
     * If any value is a percent there will be an entry here for the
     * percent value. Use percentIndex to find out the index for it.
     */
    float[] percentValues;
    int[] percentIndexs;
    /**
     * Last value of width passed in.
     */
    int lastWidth;
    /**
     * Last value of height passed in.
     */
    int lastHeight;

    public PolygonRegionContainment(AttributeSet as) {
      int[] coords = Map.extractCoords(as.getAttribute(HTML.Attribute.
          COORDS));

      if (coords == null || coords.length == 0 ||
          coords.length % 2 != 0) {
        throw new RuntimeException("Unable to parse polygon area");
      } else {
        int numPercents = 0;

        lastWidth = lastHeight = -1;
        for (int counter = coords.length - 1; counter >= 0;
            counter--) {
          if (coords[counter] < 0) {
            numPercents++;
          }
        }

        if (numPercents > 0) {
          percentIndexs = new int[numPercents];
          percentValues = new float[numPercents];
          for (int counter = coords.length - 1, pCounter = 0;
              counter >= 0; counter--) {
            if (coords[counter] < 0) {
              percentValues[pCounter] = coords[counter] /
                  -100.0f;
              percentIndexs[pCounter] = counter;
              pCounter++;
            }
          }
        } else {
          percentIndexs = null;
          percentValues = null;
        }
        npoints = coords.length / 2;
        xpoints = new int[npoints];
        ypoints = new int[npoints];

        for (int counter = 0; counter < npoints; counter++) {
          xpoints[counter] = coords[counter + counter];
          ypoints[counter] = coords[counter + counter + 1];
        }
      }
    }

    public boolean contains(int x, int y, int width, int height) {
      if (percentValues == null || (lastWidth == width &&
          lastHeight == height)) {
        return contains(x, y);
      }
      // Force the bounding box to be recalced.
      bounds = null;
      lastWidth = width;
      lastHeight = height;
      float fWidth = (float) width;
      float fHeight = (float) height;
      for (int counter = percentValues.length - 1; counter >= 0;
          counter--) {
        if (percentIndexs[counter] % 2 == 0) {
          // x
          xpoints[percentIndexs[counter] / 2] =
              (int) (percentValues[counter] * fWidth);
        } else {
          // y
          ypoints[percentIndexs[counter] / 2] =
              (int) (percentValues[counter] * fHeight);
        }
      }
      return contains(x, y);
    }
  }


  /**
   * Used to test for containment in a circular region.
   */
  static class CircleRegionContainment implements RegionContainment {

    /**
     * X origin of the circle.
     */
    int x;
    /**
     * Y origin of the circle.
     */
    int y;
    /**
     * Radius of the circle.
     */
    int radiusSquared;
    /**
     * Non-null indicates one of the values represents a percent.
     */
    float[] percentValues;
    /**
     * Last value of width passed in.
     */
    int lastWidth;
    /**
     * Last value of height passed in.
     */
    int lastHeight;

    public CircleRegionContainment(AttributeSet as) {
      int[] coords = Map.extractCoords(as.getAttribute(HTML.Attribute.
          COORDS));

      if (coords == null || coords.length != 3) {
        throw new RuntimeException("Unable to parse circular area");
      }
      x = coords[0];
      y = coords[1];
      radiusSquared = coords[2] * coords[2];
      if (coords[0] < 0 || coords[1] < 0 || coords[2] < 0) {
        lastWidth = lastHeight = -1;
        percentValues = new float[3];
        for (int counter = 0; counter < 3; counter++) {
          if (coords[counter] < 0) {
            percentValues[counter] = coords[counter] /
                -100.0f;
          } else {
            percentValues[counter] = -1.0f;
          }
        }
      } else {
        percentValues = null;
      }
    }

    public boolean contains(int x, int y, int width, int height) {
      if (percentValues != null && (lastWidth != width ||
          lastHeight != height)) {
        int newRad = Math.min(width, height) / 2;

        lastWidth = width;
        lastHeight = height;
        if (percentValues[0] != -1.0f) {
          this.x = (int) (percentValues[0] * width);
        }
        if (percentValues[1] != -1.0f) {
          this.y = (int) (percentValues[1] * height);
        }
        if (percentValues[2] != -1.0f) {
          radiusSquared = (int) (percentValues[2] *
              Math.min(width, height));
          radiusSquared *= radiusSquared;
        }
      }
      return (((x - this.x) * (x - this.x) +
          (y - this.y) * (y - this.y)) <= radiusSquared);
    }
  }


  /**
   * An implementation that will return true if the x, y location is
   * inside a rectangle defined by origin 0, 0, and width equal to
   * width passed in, and height equal to height passed in.
   */
  static class DefaultRegionContainment implements RegionContainment {

    /**
     * A global shared instance.
     */
    static DefaultRegionContainment si = null;

    public static DefaultRegionContainment sharedInstance() {
      if (si == null) {
        si = new DefaultRegionContainment();
      }
      return si;
    }

    public boolean contains(int x, int y, int width, int height) {
      return (x <= width && x >= 0 && y >= 0 && y <= width);
    }
  }
}
